feat: follow key chains when resolving foreign key columns#166
Merged
Conversation
A foreign key to an entity whose primary key is itself an entity reference resolved to a single <field>_id column instead of the referenced key's columns, breaking every operation on such entities. The key structure was derived independently — and inconsistently — by column naming, column typing and value extraction. The key chain is now reified in one place: RecordReflection.getPkLeaves flattens a primary key into its terminal fields, one per database column, each carrying the accessor path from the declaring table. Foreign keys that are primary keys recurse into the referenced entity's key, record keys contribute their components, and circular chains or missing keys fail fast at model construction with a clear message. Column naming, column emission and value extraction all consume the flattened leaves. Column gains persistedType() — the Java type of the value as it is persisted to the column (the chain's terminal type for foreign keys, the converter parameter type for converter-backed columns) — so dialects no longer need any knowledge of key structure: the H2 MERGE cast resolution reduces to a type lookup. ModelImpl precomputes per-column accessor paths at model construction, so per-row extraction is a plain loop of accessor invocations. RecordMapper's early cache lookup now only constructs the key directly when the key record is flat; chained keys are built through the regular argument plan. New H2 coverage: full CRUD and MERGE upserts on a two-level compound key chain, a single-column two-level chain, and fail-fast on circular key chains.
Schema validation checked foreign key columns against the declared entity type, which the type compatibility map does not know — the check was silently skipped. Validating against the persisted type checks the referenced key's terminal type against the database column, following key chains.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A foreign key to an entity whose primary key is itself an entity reference resolved to a single
<field>_idcolumn instead of the referenced key's columns, breaking every operation on such entities (dependent one-to-one chains). Key structure was derived independently — and inconsistently — by column naming, column typing, and value extraction.Key chains as a first-class model concept.
RecordReflection.getPkLeavesflattens a primary key into its terminal fields — oneKeyLeafper database column, each carrying the accessor path from the declaring table. Foreign keys that are primary keys recurse into the referenced entity's key, record keys contribute their components, and circular chains or missing keys fail fast at model construction with a clear message. Column naming (getForeignKeys), column emission (ModelFactory), and value extraction (ModelImpl) all consume the flattened leaves; the old one-levelgetNestedPkFieldsis gone.Column.persistedType(). The Java type of the value as it is persisted to the column: the chain's terminal type for foreign keys, the converter parameter type for converter-backed columns, the field type otherwise. Dialects no longer need any knowledge of key structure — H2's MERGE cast resolution reduced from ~40 lines with FK special-casing to a type lookup.Foreign key columns are now schema-validated. Validation previously checked FK columns against the declared entity type, which the compatibility map cannot know — the check was silently skipped. Checking
persistedType()validates the referenced key's terminal type against the database column, through chains.Performance. All flattening happens once at model construction;
ModelImplprecomputes per-column accessor paths so per-row extraction is a plain loop of MethodHandle-backed invocations, replacing the per-rowinstanceofcascade.RecordMapper's early-cache PK shortcut now only constructs the key directly when the key record is flat (constructor arity equals flat column width); chained keys build through the regular argument plan.Docs. New "Key Chains" subsection under "Primary Key as Foreign Key" in
relationships.md; matching bullet in thestorm-entity-kotlin/storm-entity-javaskills.Verification
RecordReflectionunit tests migrated togetPkLeaves, including the flipped expectation: FK-typed primary keys now flatten into the referenced key instead of stopping at the field.